﻿using System;
using System.Collections.Generic;
using System.Text;
using NGinnBPM.Lib.Data;
using System.Runtime.Serialization;

namespace NGinnBPM.Lib.Schema
{
    public enum TaskSplitType
    {
        XOR,
        AND,
        OR
    }

    /// <summary>
    /// Task output port types
    /// TODO implement missing types
    /// </summary>
    public enum TaskOutPortType
    {
        /// <summary>Standard output port</summary>
        Default,
        /// <summary>Failure output port</summary>
        Error,
        /// <summary>Signal output port (unused yet)</summary>
        Signal,
        /// <summary>Cancel output port. Token generated here after task is cancelled.</summary>
        Cancel,
        /// <summary>Compensation output port. Token generated here if a task is compensated.</summary>
        Compensate
    }

    /// <summary>
    /// Task input port type. 
    /// TODO - some sensible implementation is needed.
    /// What input ports can we have
    /// - normal
    /// - cancel (this should rather lead to a place, not a task)
    /// - compensate
    /// TODO - implement
    /// </summary>
    public enum TaskInPortType
    {
        Default
    }


    public enum NGinnTaskType
    {
        Empty,
        Manual,
        Script,
        Custom,
        RaiseError,
        ReceiveMessage,
        SendMessage,
        Subprocess,
        Timer,
        Notification,
        XmlHttp,
        Composite
    }
    /// <summary>
    /// Process task definition
    /// Task output ports should be connected to places.
    /// Cancellation/compensation
    /// Task cancellation = task input token is removed from a place and all active
    /// tasks with that input token are cancelled.
    /// Task compensation = task input token is removed from a place? No WAY:
    /// only completed tasks can be compensated!!!
    /// composite tasks can be compensated in the middle...
    /// </summary>
    [Serializable]
    [DataContract]
    [KnownType(typeof(AtomicTask))]
    public abstract class Task : NetNode
    {
        private TaskSplitType _joinType;
        private TaskSplitType _splitType;
        private string _multiInstSplitExpression;
        private string _multiInstAlias = "item";
        private string _multiInstResultExpression = null;
        private bool _autoBindVariables = true;

        public Task()
        {
            Variables = new List<VariableDef>();
            InputBindings = new List<VariableBinding>();
            OutputBindings = new List<VariableBinding>();
            InputParameterBindings = new List<VariableBinding>();
            OutputParameterBindings = new List<VariableBinding>();
        }

        [DataMember]
        public NGinnTaskType TaskType { get; set; }
        
        [DataMember]
        public TaskSplitType SplitType
        {
            get { return _splitType; }
            set { _splitType = value; }
        }

        [DataMember]
        public TaskSplitType JoinType
        {
            get { return _joinType; }
            set { _joinType = value; }
        }

        /// <summary>
        /// If true, NGinn will automatically copy input/output variable values from and to parent task
        /// if only variable names and types match. If false you have to specify bindings manually.
        /// With automatic binding you can still override the default behavior by specifying your own bindings.
        /// </summary>
        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public bool AutoBindVariables
        {
            get { return _autoBindVariables; }
            set { _autoBindVariables = value; }
        }

        [IgnoreDataMember]
        public bool IsMultiInstance
        {
            get { return _multiInstSplitExpression != null && _multiInstSplitExpression.Length > 0; }
        }

        /// <summary>
        /// Expression that will be used for generating input data for multi-instance tasks
        /// It should evaluate to a collection with number of items equal to desired number
        /// of tasks in multi-instance.
        /// </summary>
        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public string MultiInstanceSplitExpression
        {
            get { return _multiInstSplitExpression; }
            set { _multiInstSplitExpression = value; }
        }

        /// <summary>
        /// Alias for multi-instance query results item. This alias can be used for data binding 
        /// and refers to a single item in list returned by MultiInstanceSplitExpression
        /// By default the alias is 'item'.
        /// </summary>
        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public string MultiInstanceItemAlias
        {
            get { return _multiInstAlias; }
            set { _multiInstAlias = value; }
        }

        /// <summary>
        /// Binding expression for multi-instance task results.
        /// Results for multi-instance tasks are returned as arrays of DataObjects,
        /// so the binding expression must refer to an array variable. Its value will be replaced
        /// with task results.
        /// </summary>
        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public string MultiInstanceResultsBinding
        {
            get { return _multiInstResultExpression; }
            set { _multiInstResultExpression = value; }
        }

        [DataMember]
        public List<VariableDef> Variables { get; set; }
        
        public void AddVariable(VariableDef vd)
        {
            if (GetVariable(vd.Name) != null) throw new Exception("Variable already defined");
            Variables.Add(vd);
        }

        public VariableDef GetVariable(string name)
        {
            foreach (VariableDef vd in Variables)
                if (vd.Name == name) return vd;
            return null;
        }

        [DataMember]
        public List<VariableBinding> InputBindings { get; set; }
        [DataMember]
        public List<VariableBinding> OutputBindings { get; set; }
        
        public void AddInputBinding(VariableBinding vb)
        {
            if (GetInputBinding(vb.VariableName) != null) throw new Exception("Input binding already defined for " + vb.VariableName);
            InputBindings.Add(vb);
        }

        public void AddOutputBinding(VariableBinding vb)
        {
            if (GetOutputBinding(vb.VariableName) != null) throw new Exception("Output binding already defined for " + vb.VariableName);
            OutputBindings.Add(vb);
        }

        public VariableBinding GetInputBinding(string varName)
        {
            foreach (VariableBinding vb in InputBindings)
                if (vb.VariableName == varName) return vb;
            return null;
        }

        public VariableBinding GetOutputBinding(string varName)
        {
            foreach (VariableBinding vb in OutputBindings)
                if (vb.VariableName == varName) return vb;
            return null;
        }

        [DataMember]
        public List<VariableBinding> InputParameterBindings { get; set; }
        
        /// <summary>
        /// Add task input parameter binding
        /// </summary>
        /// <param name="vb"></param>
        public void AddInputParameterBinding(VariableBinding vb)
        {
            if (GetInputParameterBinding(vb.VariableName) != null) throw new Exception("Input parameter already defined: " + vb.VariableName);
            InputParameterBindings.Add(vb);
        }

        public VariableBinding GetInputParameterBinding(string varName)
        {
            foreach (VariableBinding vb in InputParameterBindings)
                if (vb.VariableName == varName) return vb;
            return null;
        }

        /// <summary>
        /// List of bindings of task parameters to task output variables.
        /// These bindings are executed after task completion, but before
        /// returning its output data. Each binding targets task variable.
        /// </summary>
        [DataMember]
        public List<VariableBinding> OutputParameterBindings { get; set; }
        
        /// <summary>
        /// Add task input parameter binding
        /// </summary>
        /// <param name="vb"></param>
        public void AddOutputParameterBinding(VariableBinding vb)
        {
            if (GetInputParameterBinding(vb.VariableName) != null) throw new Exception("Input parameter already defined: " + vb.VariableName);
            OutputParameterBindings.Add(vb);
        }

        public VariableBinding GetOutputParameterBinding(string varName)
        {
            foreach (VariableBinding vb in OutputParameterBindings)
                if (vb.VariableName == varName) return vb;
            return null;
        }

        /// <summary>
        /// List of places to be searched for tokens in case of OR join
        /// </summary>
        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public List<string> ORJoinChecklist { get; set; }
        
        /// <summary>
        /// Returns list of flows out of specified port
        /// ordered by eval order
        /// </summary>
        /// <param name="port"></param>
        /// <returns></returns>
        public IList<Flow> GetFlowsForPortOut(TaskOutPortType port)
        {
            List<Flow> lst = new List<Flow>();
            foreach (Flow fl in FlowsOut)
            {
                if (fl.SourcePort == port)
                    lst.Add(fl);
            }
            lst.Sort(new Comparison<Flow>(CompareFlowOrder));
            return lst;
        }

        public override bool Validate(IList<ValidationMessage> msgs)
        {
            int cnt = msgs.Count;
            base.Validate(msgs);
            if (IsMultiInstance)
            {
                if (MultiInstanceItemAlias == null || MultiInstanceItemAlias.Length == 0)
                    msgs.Add(new ValidationMessage(true, "MultiInstanceItemAlias missing for multi-instance task", this));
            }
            if (JoinType == TaskSplitType.OR)
            {
                if (ORJoinChecklist.Count == 0 && FlowsIn.Count > 1)
                    msgs.Add(new ValidationMessage(false, "OR-join task should have OR-join checklist defined", this));
            }
            for (int i = cnt; i < msgs.Count; i++)
                if (msgs[i].IsError) return false;
            return true;
        }

        /// <summary>
        /// Get task input data definition
        /// </summary>
        /// <returns></returns>
        public virtual StructDef GetInputDataSchema()
        {
            if (ParentProcessDefinition == null) throw new Exception();
            StructDef sd = new StructDef();
            sd.ParentTypeSet = ParentProcessDefinition.DataTypes;
            foreach (VariableDef vd in Variables)
            {
                if (vd.VariableDir == VariableDef.Dir.In || vd.VariableDir == VariableDef.Dir.InOut)
                {
                    sd.Members.Add(vd);
                }
            }
            return sd;
        }

        /// <summary>
        /// Get task output data definition
        /// </summary>
        /// <returns></returns>
        public virtual StructDef GetOutputDataSchema()
        {
            if (ParentProcessDefinition == null) throw new Exception();
            StructDef sd = new StructDef();
            sd.ParentTypeSet = ParentProcessDefinition.DataTypes;
            foreach (VariableDef vd in Variables)
            {
                if (vd.VariableDir == VariableDef.Dir.Out || vd.VariableDir == VariableDef.Dir.InOut)
                {
                    sd.Members.Add(vd);
                }
            }
            return sd;
        }

        /// <summary>
        /// Get the definition of internal task data structure (all variables)
        /// Warning: there are no required fields in the internal data schema. So any variable, even the required ones, can be skipped
        /// in an xml document.
        /// </summary>
        /// <returns></returns>
        public virtual StructDef GetInternalDataSchema()
        {
            if (ParentProcessDefinition == null) throw new Exception();
            StructDef sd = new StructDef();
            sd.ParentTypeSet = ParentProcessDefinition.DataTypes;
            foreach (VariableDef vd in Variables)
            {
                VariableDef vd2 = new VariableDef(vd); vd2.IsRequired = false;
                sd.Members.Add(vd2);
            }
            return sd;
        }

        public string GetInputDataXmlSchema()
        {
            StructDef sd = GetInputDataSchema();
            return ParentProcessDefinition.DataTypes.GetDataSchema(sd, Id, null);
        }

        public string GetOutputDataXmlSchema()
        {
            StructDef sd = GetOutputDataSchema();
            return ParentProcessDefinition.DataTypes.GetDataSchema(sd, Id, null);
        }

        public string GetInternalDataXmlSchema()
        {
            StructDef sd = GetInternalDataSchema();
            return ParentProcessDefinition.DataTypes.GetDataSchema(sd, Id, null);
        }

        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public string BeforeEnableScript { get; set; }

        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public string AfterEnableScript { get; set; }

        [DataMember(IsRequired = false, EmitDefaultValue = false)]
        public string BeforeCompleteScript { get; set; }
    }
}
